<?php

class RainTPL {

    static $tpl_dir = "tpl/";
    static $cache_dir = "template/";

    /**
     * Template base URL. RainTPL will add this URL to the relative paths of element selected in $path_replace_list.
     *
     * @var string
     */
    static $base_url = null;
    static $tpl_ext = "html";

    /**
     * Path replace is a cool features that replace all relative paths of images (<img src="...">), stylesheet (<link href="...">), script (<script src="...">) and link (<a href="...">)
     * Set true to enable the path replace.
     *
     * @var unknown_type
     */
    static $path_replace = true;
    static $path_replace_list = array('a', 'img', 'link', 'script', 'input');

    /**
     * You can define in the black list what string are disabled into the template tags
     *
     * @var unknown_type
     */
    static $black_list = array('\$this', 'raintpl::', 'self::', '_SESSION', '_SERVER', '_ENV', 'eval', 'exec', 'unlink', 'rmdir');

    /**
     * Check template.
     * true: checks template update time, if changed it compile them
     * false: loads the compiled template. Set false if server doesn't have write permission for cache_directory.
     *
     */
    static $check_template_update = true;
    static $php_enabled = false;

    /**
     * Debug mode flag.
     * True: debug mode is used, syntax errors are displayed directly in template. Execution of script is not terminated.
     * False: exception is thrown on found error.
     *
     * @var bool
     */
    static $debug = true;
    public $var = array();
    protected $tpl = array(), // variables to keep the template directories and info
            $cache = false, // static cache enabled / disabled
            $cache_id = null;       // identify only one cache
    protected static $config_name_sum = array();   // takes all the config to create the md5 of the file

    // -------------------------

    const CACHE_EXPIRE_TIME = 3600; // default cache expire time = hour

    function assign($variable, $value = null) {
        if (is_array($variable))
            $this->var += $variable;
        else
            $this->var[$variable] = $value;
    }

    /**
     * Draw the template
     * eg. 	$html = $tpl->draw( 'demo', TRUE ); // return template in string
     * or 	$tpl->draw( $tpl_name ); // echo the template
     *
     * @param string $tpl_name  template to load
     * @param boolean $return_string  true=return a string, false=echo the template
     * @return string
     */
    function draw($tpl_name, $return_string = true) {

        try {
            // compile the template if necessary and set the template filepath
            $this->check_template($tpl_name);
        } catch (RainTpl_Exception $e) {
            $output = $this->printDebug($e);
            die($output);
        }

        // Cache is off and, return_string is false
        // Rain just echo the template

        if (!$this->cache && !$return_string) {
            extract($this->var);
            include $this->tpl['compiled_filename'];
            unset($this->tpl);
        } else {
            // cache or return_string are enabled
            // rain get the output buffer to save the output in the cache or to return it as string
            //----------------------
            // get the output buffer
            //----------------------
            ob_start();
            extract($this->var);
            include $this->tpl['compiled_filename'];
            $raintpl_contents = ob_get_clean();
            //----------------------
            // save the output in the cache
            if ($this->cache)
                file_put_contents($this->tpl['cache_filename'], "<?php if(!class_exists('raintpl')){exit;}?>" . $raintpl_contents);

            // free memory
            unset($this->tpl);

            // return or print the template
            if ($return_string)
                return $raintpl_contents;
            else
                echo $raintpl_contents;
        }
    }

    /**
     * If exists a valid cache for this template it returns the cache
     *
     * @param string $tpl_name Name of template (set the same of draw)
     * @param int $expiration_time Set after how many seconds the cache expire and must be regenerated
     * @return string it return the HTML or null if the cache must be recreated
     */
    function cache($tpl_name, $expire_time = self::CACHE_EXPIRE_TIME, $cache_id = null) {

        // set the cache_id
        $this->cache_id = $cache_id;

        if (!$this->check_template($tpl_name) && file_exists($this->tpl['cache_filename']) && ( time() - filemtime($this->tpl['cache_filename']) < $expire_time ))
            return substr(file_get_contents($this->tpl['cache_filename']), 43);
        else {
            //delete the cache of the selected template
            if (file_exists($this->tpl['cache_filename']))
                unlink($this->tpl['cache_filename']);
            //$this->cache = true;
        }
    }

    /**
     * Configure the settings of RainTPL
     *
     */
    static function configure($setting, $value = null) {
        if (is_array($setting))
            foreach ($setting as $key => $value)
                self::configure($key, $value);
        else if (property_exists(__CLASS__, $setting)) {
            self::$$setting = $value;
            self::$config_name_sum[$setting] = $value; // take trace of all config
        }
    }

    // check if has to compile the template
    // return true if the template has changed
    protected function check_template($tpl_name) {

        if (!isset($this->tpl['checked'])) {
            $example = new AlaniaContenidosModel();
            $db = new connection();
            if (gettype($tpl_name) == "string") {

                $example->setNombre($this->id);
            } else {
                $example->setId($this->id);
            }
            /* if (!count(AlaniaContenidosModel::findByExample($db->db, $example))) {
              return "Error loading template file ($this->id).<br />";
              } */
            if (self::$check_template_update && !count(AlaniaContenidosModel::findByExample($db->db, $example))) {
                $e = new RainTpl_NotFoundException('Template ' . $tpl_basename . ' not found!');
                throw $e->setTemplateFile($this->tpl['tpl_filename']);
            }
            foreach (AlaniaContenidosModel::findByExample($db->db, $example) as $member) {
                $output = $member->getPlantilla();
                $this->id = $member->getId();
                $tpl_basename = $member->getNombre();

                //$tpl_basename = basename($tpl_name);              // template basename
                //$tpl_basedir = strpos($tpl_name, "/") ? dirname($tpl_name) . '/' : null;      // template basedirectory
                $tpl_dir = self::$tpl_dir . $tpl_basedir;        // template directory
                //$this->tpl['tpl_filename'] = $tpl_dir . $tpl_basename . '.' . self::$tpl_ext; // template filename
                $temp_compiled_filename = self::$cache_dir . $tpl_basename . "." . md5($tpl_dir . serialize(self::$config_name_sum));
                $this->tpl['compiled_filename'] = $temp_compiled_filename . '.rtpl.php'; // cache filename
                $this->tpl['cache_filename'] = $temp_compiled_filename . '.s_' . $this->cache_id . '.rtpl.php'; // static cache filename
                // if the template doesn't exsist throw an error
                /* if (self::$check_template_update && !file_exists($this->tpl['tpl_filename'])) {
                  $e = new RainTpl_NotFoundException('Template ' . $tpl_basename . ' not found!');
                  throw $e->setTemplateFile($this->tpl['tpl_filename']);
                  } */

                // file doesn't exsist, or the template was updated, Rain will compile the template
                if (!file_exists($this->tpl['compiled_filename']) || ( self::$check_template_update && filemtime($this->tpl['compiled_filename']) < filemtime($this->tpl['tpl_filename']) )) {
                    $this->compileFile($member);//, $tpl_basedir, $this->tpl['tpl_filename'], self::$cache_dir, $this->tpl['compiled_filename']);
                    return true;
                }
                $this->tpl['checked'] = true;
            }
        }
    }

    /**
     * execute stripslaches() on the xml block. Invoqued by preg_replace_callback function below
     * @access protected
     */
    protected function xml_reSubstitution($capture) {
        return "<?php echo '<?xml " . stripslashes($capture[1]) . " ?>'; ?>";
    }

    /**
     * Compile and write the compiled template file
     * @access protected
     */
    protected function compileFile(AlaniaContenidosModel $contenido) {//$tpl_basename, $tpl_basedir, $tpl_filename, $cache_dir, $compiled_filename) {
        //read template file
        $this->tpl['source'] = $template_code = $contenido->getPlantilla(); //file_get_contents($tpl_filename);
        //xml substitution
        $template_code = preg_replace("/<\?xml(.*?)\?>/s", "##XML\\1XML##", $template_code);

        //disable php tag
        if (!self::$php_enabled)
            $template_code = str_replace(array("<?", "?>"), array("&lt;?", "?&gt;"), $template_code);

        //xml re-substitution
        $template_code = preg_replace_callback("/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code);

        //compile template
        $template_compiled = "<?php if(!class_exists('raintpl')){exit;}?>" . $this->compileTemplate($template_code);//, $tpl_basedir);


        // fix the php-eating-newline-after-closing-tag-problem
        $template_compiled = str_replace("?>\n", "?>\n\n", $template_compiled);

        // create directories
        if (!is_dir(self::$cache_dir))
            mkdir(self::$cache_dir, 0755, true);

        if (!is_writable($cache_dir))
            throw new RainTpl_Exception('Cache directory ' . $cache_dir . 'doesn\'t have write permission. Set write permission or set RAINTPL_CHECK_TEMPLATE_UPDATE to false. More details on http://www.raintpl.com/Documentation/Documentation-for-PHP-developers/Configuration/');

        //write compiled file
        file_put_contents($this->tpl['compiled_filename'], $template_compiled);
    }

    /**
     * Compile template
     * @access protected
     */
    protected function compileTemplate($template_code){//, $tpl_basedir) {

        //tag list
        $tag_regexp = array('loop' => '(\{loop(?: name){0,1}="\${0,1}[^"]*"\})',
            'loop_close' => '(\{\/loop\})',
            'if' => '(\{if(?: condition){0,1}="[^"]*"\})',
            'elseif' => '(\{elseif(?: condition){0,1}="[^"]*"\})',
            'else' => '(\{else\})',
            'if_close' => '(\{\/if\})',
            'function' => '(\{function="[^"]*"\})',
            'noparse' => '(\{noparse\})',
            'noparse_close' => '(\{\/noparse\})',
            'ignore' => '(\{ignore\}|\{\*)',
            'ignore_close' => '(\{\/ignore\}|\*\})',
            'include' => '(\{include="[^"]*"(?: cache="[^"]*")?\})',
            'template_info' => '(\{\$template_info\})',
            'function' => '(\{function="(\w*?)(?:.*?)"\})'
        );



        $tag_regexp = "/" . join("|", $tag_regexp) . "/";

        //split the code with the tags regexp
        $template_code = preg_split($tag_regexp, $template_code, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);

        //path replace (src of img, background and href of link)
        $template_code = $this->path_replace($template_code);//, $tpl_basedir);

        //compile the code
        $compiled_code = $this->compileCode($template_code);

        //return the compiled code
        return $compiled_code;
    }

    /**
     * Compile the code
     * @access protected
     */
    protected function compileCode($parsed_code) {

        //variables initialization
        $compiled_code = $open_if = $comment_is_open = $ignore_is_open = null;
        $loop_level = 0;

        //read all parsed code
        while ($html = array_shift($parsed_code)) {
            //var_dump($html);
            //close ignore tag
            if (!$comment_is_open && ( strpos($html, '{/ignore}') !== FALSE || strpos($html, '*}') !== FALSE ))
                $ignore_is_open = false;

            //code between tag ignore id deleted
            elseif ($ignore_is_open) {
                //ignore the code
            }

            //close no parse tag
            elseif (strpos($html, '{/noparse}') !== FALSE)
                $comment_is_open = false;

            //code between tag noparse is not compiled
            elseif ($comment_is_open)
                $compiled_code .= $html;

            //ignore
            elseif (strpos($html, '{ignore}') !== FALSE || strpos($html, '{*') !== FALSE)
                $ignore_is_open = true;

            //noparse
            elseif (strpos($html, '{noparse}') !== FALSE)
                $comment_is_open = true;

            //include tag
            elseif (preg_match('/\{include="([^"]*)"(?: cache="([^"]*)"){0,1}\}/', $html, $code)) {

                //variables substitution
                $include_var = $this->var_replace($code[1], $left_delimiter = null, $right_delimiter = null, $php_left_delimiter = '".', $php_right_delimiter = '."', $loop_level);

                // if the cache is active
                if (isset($code[2])) {

                    //dynamic include
                    $compiled_code .= '<?php $tpl = new ' . get_class($this) . ';' .
                            'if( $cache = $tpl->cache( $template = basename("' . $include_var . '") ) )' .
                            '	echo $cache;' .
                            'else{' .
                            '	$tpl_dir_temp = self::$tpl_dir;' .
                            '	$tpl->assign( $this->var );' .
                            (!$loop_level ? null : '$tpl->assign( "key", $key' . $loop_level . ' ); $tpl->assign( "value", $value' . $loop_level . ' );' ) .
                            '	$tpl->draw( dirname("' . $include_var . '") . ( substr("' . $include_var . '",-1,1) != "/" ? "/" : "" ) . basename("' . $include_var . '") );' .
                            '} ?>';
                } else {

                    //dynamic include
                    $compiled_code .= '<?php $tpl = new ' . get_class($this) . ';' .
                            '$tpl_dir_temp = self::$tpl_dir;' .
                            '$tpl->assign( $this->var );' .
                            (!$loop_level ? null : '$tpl->assign( "key", $key' . $loop_level . ' ); $tpl->assign( "value", $value' . $loop_level . ' );' ) .
                            '$tpl->draw( dirname("' . $include_var . '") . ( substr("' . $include_var . '",-1,1) != "/" ? "/" : "" ) . basename("' . $include_var . '") );' .
                            '?>';
                }
            }

            //loop
            elseif (preg_match('/\{loop(?: name){0,1}="\${0,1}([^"]*)"\}/', $html, $code)) {


                //increase the loop counter
                $loop_level++;

                //replace the variable in the loop
                $var = $this->var_replace('$' . $code[1], $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level - 1);

                //loop variables
                $counter = "\$counter$loop_level";       // count iteration
                $key = "\$key$loop_level";               // key
                $value = "\$value$loop_level";           // value
                //loop code
                $compiled_code .= "<?php $counter=-1; if( isset($var) && is_array($var) && sizeof($var) ) foreach( $var as $key => $value ){ $counter++; ?>";
            }

            //close loop tag
            elseif (strpos($html, '{/loop}') !== FALSE) {

                //iterator
                $counter = "\$counter$loop_level";

                //decrease the loop counter
                $loop_level--;

                //close loop code
                $compiled_code .= "<?php } ?>";
            }

            //if
            elseif (preg_match('/\{if(?: condition){0,1}="([^"]*)"\}/', $html, $code)) {

                //increase open if counter (for intendation)
                $open_if++;

                //tag
                $tag = $code[0];

                //condition attribute
                $condition = $code[1];

                // check if there's any function disabled by black_list
                $this->function_check($tag);

                //variable substitution into condition (no delimiter into the condition)
                $parsed_condition = $this->var_replace($condition, $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level);

                //if code
                $compiled_code .= "<?php if( $parsed_condition ){ ?>";
            }

            //elseif
            elseif (preg_match('/\{elseif(?: condition){0,1}="([^"]*)"\}/', $html, $code)) {

                //tag
                $tag = $code[0];

                //condition attribute
                $condition = $code[1];

                //variable substitution into condition (no delimiter into the condition)
                $parsed_condition = $this->var_replace($condition, $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level);

                //elseif code
                $compiled_code .= "<?php }elseif( $parsed_condition ){ ?>";
            }

            //else
            elseif (strpos($html, '{else}') !== FALSE) {

                //else code
                $compiled_code .= '<?php }else{ ?>';
            }

            //close if tag
            elseif (strpos($html, '{/if}') !== FALSE) {

                //decrease if counter
                $open_if--;

                // close if code
                $compiled_code .= '<?php } ?>';
            }

            //function
            elseif (preg_match('/\{function="(\w*)(.*?)"\}/', $html, $code)) {

                //tag
                $tag = $code[0];

                //function
                $function = $code[1];

                // check if there's any function disabled by black_list
                $this->function_check($tag);

                if (empty($code[2]))
                    $parsed_function = $function . "()";
                else
                // parse the function
                    $parsed_function = $function . $this->var_replace($code[2], $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level);

                //if code
                $compiled_code .= "<?php echo $parsed_function; ?>";
            }

            // show all vars
            elseif (strpos($html, '{$template_info}') !== FALSE) {

                //tag
                $tag = '{$template_info}';

                //if code
                $compiled_code .= '<?php echo "<pre>"; print_r( $this->var ); echo "</pre>"; ?>';
            }


            //all html code
            else {

                //variables substitution (es. {$title})
                $html = $this->var_replace($html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true);
                //const substitution (es. {#CONST#})
                $html = $this->const_replace($html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true);
                //functions substitution (es. {"string"|functions})
                $compiled_code .= $this->func_replace($html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true);
            }
        }

        if ($open_if > 0) {
            $e = new RainTpl_SyntaxException('Error! You need to close an {if} tag in ' . $this->tpl['tpl_filename'] . ' template');
            throw $e->setTemplateFile($this->tpl['tpl_filename']);
        }
        return $compiled_code;
    }

    /**
     * Reduce a path, eg. www/library/../filepath//file => www/filepath/file
     * @param type $path
     * @return type
     */
    protected function reduce_path($path) {
        $path = str_replace("://", "@not_replace@", $path);
        $path = str_replace("//", "/", $path);
        $path = str_replace("@not_replace@", "://", $path);
        return preg_replace('/\w+\/\.\.\//', '', $path);
    }

    /**
     * replace the path of image src, link href and a href.
     * url => template_dir/url
     * url# => url
     * http://url => http://url
     *
     * @param string $html
     * @return string html sostituito
     */
    protected function path_replace($html, $tpl_basedir) {

        if (self::$path_replace) {

            $tpl_dir = self::$base_url . self::$tpl_dir . $tpl_basedir;

            // reduce the path
            $path = $this->reduce_path($tpl_dir);

            $exp = $sub = array();

            if (in_array("img", self::$path_replace_list)) {
                $exp = array('/<img(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<img(.*?)src=(?:")([^"]+?)#(?:")/i', '/<img(.*?)src="(.*?)"/', '/<img(.*?)src=(?:\@)([^"]+?)(?:\@)/i');
                $sub = array('<img$1src=@$2://$3@', '<img$1src=@$2@', '<img$1src="' . $path . '$2"', '<img$1src="$2"');
            }

            if (in_array("script", self::$path_replace_list)) {
                $exp = array_merge($exp, array('/<script(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<script(.*?)src=(?:")([^"]+?)#(?:")/i', '/<script(.*?)src="(.*?)"/', '/<script(.*?)src=(?:\@)([^"]+?)(?:\@)/i'));
                $sub = array_merge($sub, array('<script$1src=@$2://$3@', '<script$1src=@$2@', '<script$1src="' . $path . '$2"', '<script$1src="$2"'));
            }

            if (in_array("link", self::$path_replace_list)) {
                $exp = array_merge($exp, array('/<link(.*?)href=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<link(.*?)href=(?:")([^"]+?)#(?:")/i', '/<link(.*?)href="(.*?)"/', '/<link(.*?)href=(?:\@)([^"]+?)(?:\@)/i'));
                $sub = array_merge($sub, array('<link$1href=@$2://$3@', '<link$1href=@$2@', '<link$1href="' . $path . '$2"', '<link$1href="$2"'));
            }

            if (in_array("a", self::$path_replace_list)) {
                $exp = array_merge($exp, array('/<a(.*?)href=(?:")(http\:\/\/|https\:\/\/|javascript:)([^"]+?)(?:")/i', '/<a(.*?)href="(.*?)"/', '/<a(.*?)href=(?:\@)([^"]+?)(?:\@)/i'));
                $sub = array_merge($sub, array('<a$1href=@$2$3@', '<a$1href="' . self::$base_url . '$2"', '<a$1href="$2"'));
            }

            if (in_array("input", self::$path_replace_list)) {
                $exp = array_merge($exp, array('/<input(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<input(.*?)src=(?:")([^"]+?)#(?:")/i', '/<input(.*?)src="(.*?)"/', '/<input(.*?)src=(?:\@)([^"]+?)(?:\@)/i'));
                $sub = array_merge($sub, array('<input$1src=@$2://$3@', '<input$1src=@$2@', '<input$1src="' . $path . '$2"', '<input$1src="$2"'));
            }

            return preg_replace($exp, $sub, $html);
        }
        else
            return $html;
    }

    // replace const
    function const_replace($html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null) {
        // const
        return preg_replace('/\{\#(\w+)\#{0,1}\}/', $php_left_delimiter . ( $echo ? " echo " : null ) . '\\1' . $php_right_delimiter, $html);
    }

    // replace functions/modifiers on constants and strings
    function func_replace($html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null) {

        preg_match_all('/' . '\{\#{0,1}(\"{0,1}.*?\"{0,1})(\|\w.*?)\#{0,1}\}' . '/', $html, $matches);

        for ($i = 0, $n = count($matches[0]); $i < $n; $i++) {

            //complete tag ex: {$news.title|substr:0,100}
            $tag = $matches[0][$i];

            //variable name ex: news.title
            $var = $matches[1][$i];

            //function and parameters associate to the variable ex: substr:0,100
            $extra_var = $matches[2][$i];

            // check if there's any function disabled by black_list
            $this->function_check($tag);

            $extra_var = $this->var_replace($extra_var, null, null, null, null, $loop_level);


            // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
            $is_init_variable = preg_match("/^(\s*?)\=[^=](.*?)$/", $extra_var);

            //function associate to variable
            $function_var = ( $extra_var and $extra_var[0] == '|') ? substr($extra_var, 1) : null;

            //variable path split array (ex. $news.title o $news[title]) or object (ex. $news->title)
            $temp = preg_split("/\.|\[|\-\>/", $var);

            //variable name
            $var_name = $temp[0];

            //variable path
            $variable_path = substr($var, strlen($var_name));

            //parentesis transform [ e ] in [" e in "]
            $variable_path = str_replace('[', '["', $variable_path);
            $variable_path = str_replace(']', '"]', $variable_path);

            //transform .$variable in ["$variable"]
            $variable_path = preg_replace('/\.\$(\w+)/', '["$\\1"]', $variable_path);

            //transform [variable] in ["variable"]
            $variable_path = preg_replace('/\.(\w+)/', '["\\1"]', $variable_path);

            //if there's a function
            if ($function_var) {

                // check if there's a function or a static method and separate, function by parameters
                $function_var = str_replace("::", "@double_dot@", $function_var);

                // get the position of the first :
                if ($dot_position = strpos($function_var, ":")) {

                    // get the function and the parameters
                    $function = substr($function_var, 0, $dot_position);
                    $params = substr($function_var, $dot_position + 1);
                } else {

                    //get the function
                    $function = str_replace("@double_dot@", "::", $function_var);
                    $params = null;
                }

                // replace back the @double_dot@ with ::
                $function = str_replace("@double_dot@", "::", $function);
                $params = str_replace("@double_dot@", "::", $params);
            }
            else
                $function = $params = null;

            $php_var = $var_name . $variable_path;

            // compile the variable for php
            if (isset($function)) {
                if ($php_var)
                    $php_var = $php_left_delimiter . (!$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter;
                else
                    $php_var = $php_left_delimiter . (!$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $params ) )" : "$function()" ) . $php_right_delimiter;
            }
            else
                $php_var = $php_left_delimiter . (!$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter;

            $html = str_replace($tag, $php_var, $html);
        }

        return $html;
    }

    function var_replace($html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null) {

        //all variables
        if (preg_match_all('/' . $tag_left_delimiter . '\$(\w+(?:\.\${0,1}[A-Za-z0-9_]+)*(?:(?:\[\${0,1}[A-Za-z0-9_]+\])|(?:\-\>\${0,1}[A-Za-z0-9_]+))*)(.*?)' . $tag_right_delimiter . '/', $html, $matches)) {

            for ($parsed = array(), $i = 0, $n = count($matches[0]); $i < $n; $i++)
                $parsed[$matches[0][$i]] = array('var' => $matches[1][$i], 'extra_var' => $matches[2][$i]);

            foreach ($parsed as $tag => $array) {

                //variable name ex: news.title
                $var = $array['var'];

                //function and parameters associate to the variable ex: substr:0,100
                $extra_var = $array['extra_var'];

                // check if there's any function disabled by black_list
                $this->function_check($tag);

                $extra_var = $this->var_replace($extra_var, null, null, null, null, $loop_level);

                // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
                $is_init_variable = preg_match("/^[a-z_A-Z\.\[\](\-\>)]*=[^=]*$/", $extra_var);

                //function associate to variable
                $function_var = ( $extra_var and $extra_var[0] == '|') ? substr($extra_var, 1) : null;

                //variable path split array (ex. $news.title o $news[title]) or object (ex. $news->title)
                $temp = preg_split("/\.|\[|\-\>/", $var);

                //variable name
                $var_name = $temp[0];

                //variable path
                $variable_path = substr($var, strlen($var_name));

                //parentesis transform [ e ] in [" e in "]
                $variable_path = str_replace('[', '["', $variable_path);
                $variable_path = str_replace(']', '"]', $variable_path);

                //transform .$variable in ["$variable"] and .variable in ["variable"]
                $variable_path = preg_replace('/\.(\${0,1}\w+)/', '["\\1"]', $variable_path);

                // if is an assignment also assign the variable to $this->var['value']
                if ($is_init_variable)
                    $extra_var = "=\$this->var['{$var_name}']{$variable_path}" . $extra_var;



                //if there's a function
                if ($function_var) {

                    // check if there's a function or a static method and separate, function by parameters
                    $function_var = str_replace("::", "@double_dot@", $function_var);


                    // get the position of the first :
                    if ($dot_position = strpos($function_var, ":")) {

                        // get the function and the parameters
                        $function = substr($function_var, 0, $dot_position);
                        $params = substr($function_var, $dot_position + 1);
                    } else {

                        //get the function
                        $function = str_replace("@double_dot@", "::", $function_var);
                        $params = null;
                    }

                    // replace back the @double_dot@ with ::
                    $function = str_replace("@double_dot@", "::", $function);
                    $params = str_replace("@double_dot@", "::", $params);
                }
                else
                    $function = $params = null;

                //if it is inside a loop
                if ($loop_level) {
                    //verify the variable name
                    if ($var_name == 'key')
                        $php_var = '$key' . $loop_level;
                    elseif ($var_name == 'value')
                        $php_var = '$value' . $loop_level . $variable_path;
                    elseif ($var_name == 'counter')
                        $php_var = '$counter' . $loop_level;
                    else
                        $php_var = '$' . $var_name . $variable_path;
                }
                else
                    $php_var = '$' . $var_name . $variable_path;

                // compile the variable for php
                if (isset($function))
                    $php_var = $php_left_delimiter . (!$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter;
                else
                    $php_var = $php_left_delimiter . (!$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter;

                $html = str_replace($tag, $php_var, $html);
            }
        }

        return $html;
    }

    /**
     * Check if function is in black list (sandbox)
     *
     * @param string $code
     * @param string $tag
     */
    protected function function_check($code) {

        $preg = '#(\W|\s)' . implode('(\W|\s)|(\W|\s)', self::$black_list) . '(\W|\s)#';

        // check if the function is in the black list (or not in white list)
        if (count(self::$black_list) && preg_match($preg, $code, $match)) {

            // find the line of the error
            $line = 0;
            $rows = explode("\n", $this->tpl['source']);
            while (!strpos($rows[$line], $code))
                $line++;

            // stop the execution of the script
            $e = new RainTpl_SyntaxException('Unallowed syntax in ' . $this->tpl['tpl_filename'] . ' template');
            throw $e->setTemplateFile($this->tpl['tpl_filename'])
                    ->setTag($code)
                    ->setTemplateLine($line);
        }
    }

    /**
     * Prints debug info about exception or passes it further if debug is disabled.
     *
     * @param RainTpl_Exception $e
     * @return string
     */
    protected function printDebug(RainTpl_Exception $e) {
        if (!self::$debug) {
            throw $e;
        }
        $output = sprintf('<h2>Exception: %s</h2><h3>%s</h3><p>template: %s</p>', get_class($e), $e->getMessage(), $e->getTemplateFile()
        );
        if ($e instanceof RainTpl_SyntaxException) {
            if (null != $e->getTemplateLine()) {
                $output .= '<p>line: ' . $e->getTemplateLine() . '</p>';
            }
            if (null != $e->getTag()) {
                $output .= '<p>in tag: ' . htmlspecialchars($e->getTag()) . '</p>';
            }
            if (null != $e->getTemplateLine() && null != $e->getTag()) {
                $rows = explode("\n", htmlspecialchars($this->tpl['source']));
                $rows[$e->getTemplateLine()] = '<font color=red>' . $rows[$e->getTemplateLine()] . '</font>';
                $output .= '<h3>template code</h3>' . implode('<br />', $rows) . '</pre>';
            }
        }
        $output .= sprintf('<h3>trace</h3><p>In %s on line %d</p><pre>%s</pre>', $e->getFile(), $e->getLine(), nl2br(htmlspecialchars($e->getTraceAsString()))
        );
        return $output;
    }

}

/**
 * Basic Rain tpl exception.
 */
class RainTpl_Exception extends Exception {

    /**
     * Path of template file with error.
     */
    protected $templateFile = '';

    /**
     * Returns path of template file with error.
     *
     * @return string
     */
    public function getTemplateFile() {
        return $this->templateFile;
    }

    /**
     * Sets path of template file with error.
     *
     * @param string $templateFile
     * @return RainTpl_Exception
     */
    public function setTemplateFile($templateFile) {
        $this->templateFile = (string) $templateFile;
        return $this;
    }

}

/**
 * Exception thrown when template file does not exists.
 */
class RainTpl_NotFoundException extends RainTpl_Exception {
    
}

/**
 * Exception thrown when syntax error occurs.
 */
class RainTpl_SyntaxException extends RainTpl_Exception {

    /**
     * Line in template file where error has occured.
     *
     * @var int | null
     */
    protected $templateLine = null;

    /**
     * Tag which caused an error.
     *
     * @var string | null
     */
    protected $tag = null;

    /**
     * Returns line in template file where error has occured
     * or null if line is not defined.
     *
     * @return int | null
     */
    public function getTemplateLine() {
        return $this->templateLine;
    }

    /**
     * Sets  line in template file where error has occured.
     *
     * @param int $templateLine
     * @return RainTpl_SyntaxException
     */
    public function setTemplateLine($templateLine) {
        $this->templateLine = (int) $templateLine;
        return $this;
    }

    /**
     * Returns tag which caused an error.
     *
     * @return string
     */
    public function getTag() {
        return $this->tag;
    }

    /**
     * Sets tag which caused an error.
     *
     * @param string $tag
     * @return RainTpl_SyntaxException
     */
    public function setTag($tag) {
        $this->tag = (string) $tag;
        return $this;
    }

}

// -- end
